// Copyright 2013 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package adwords.axis.v201302.advancedoperations;

import com.google.api.ads.adwords.axis.factory.AdWordsServices;
import com.google.api.ads.adwords.axis.v201302.cm.AttributeFieldMapping;
import com.google.api.ads.adwords.axis.v201302.cm.CampaignAdExtension;
import com.google.api.ads.adwords.axis.v201302.cm.CampaignAdExtensionOperation;
import com.google.api.ads.adwords.axis.v201302.cm.CampaignAdExtensionPage;
import com.google.api.ads.adwords.axis.v201302.cm.CampaignAdExtensionReturnValue;
import com.google.api.ads.adwords.axis.v201302.cm.CampaignAdExtensionServiceInterface;
import com.google.api.ads.adwords.axis.v201302.cm.CampaignAdExtensionStatus;
import com.google.api.ads.adwords.axis.v201302.cm.CampaignFeed;
import com.google.api.ads.adwords.axis.v201302.cm.CampaignFeedOperation;
import com.google.api.ads.adwords.axis.v201302.cm.CampaignFeedServiceInterface;
import com.google.api.ads.adwords.axis.v201302.cm.ConstantOperand;
import com.google.api.ads.adwords.axis.v201302.cm.ConstantOperandConstantType;
import com.google.api.ads.adwords.axis.v201302.cm.Feed;
import com.google.api.ads.adwords.axis.v201302.cm.FeedAttribute;
import com.google.api.ads.adwords.axis.v201302.cm.FeedAttributeType;
import com.google.api.ads.adwords.axis.v201302.cm.FeedItem;
import com.google.api.ads.adwords.axis.v201302.cm.FeedItemAttributeValue;
import com.google.api.ads.adwords.axis.v201302.cm.FeedItemOperation;
import com.google.api.ads.adwords.axis.v201302.cm.FeedItemReturnValue;
import com.google.api.ads.adwords.axis.v201302.cm.FeedItemServiceInterface;
import com.google.api.ads.adwords.axis.v201302.cm.FeedMapping;
import com.google.api.ads.adwords.axis.v201302.cm.FeedMappingOperation;
import com.google.api.ads.adwords.axis.v201302.cm.FeedMappingPage;
import com.google.api.ads.adwords.axis.v201302.cm.FeedMappingServiceInterface;
import com.google.api.ads.adwords.axis.v201302.cm.FeedOperation;
import com.google.api.ads.adwords.axis.v201302.cm.FeedOrigin;
import com.google.api.ads.adwords.axis.v201302.cm.FeedReturnValue;
import com.google.api.ads.adwords.axis.v201302.cm.FeedServiceInterface;
import com.google.api.ads.adwords.axis.v201302.cm.Function;
import com.google.api.ads.adwords.axis.v201302.cm.FunctionArgumentOperand;
import com.google.api.ads.adwords.axis.v201302.cm.FunctionOperator;
import com.google.api.ads.adwords.axis.v201302.cm.Operator;
import com.google.api.ads.adwords.axis.v201302.cm.Predicate;
import com.google.api.ads.adwords.axis.v201302.cm.PredicateOperator;
import com.google.api.ads.adwords.axis.v201302.cm.RequestContextOperand;
import com.google.api.ads.adwords.axis.v201302.cm.RequestContextOperandContextType;
import com.google.api.ads.adwords.axis.v201302.cm.Selector;
import com.google.api.ads.adwords.axis.v201302.cm.Sitelink;
import com.google.api.ads.adwords.axis.v201302.cm.SitelinksExtension;
import com.google.api.ads.adwords.lib.client.AdWordsSession;
import com.google.api.ads.common.lib.auth.OfflineCredentials;
import com.google.api.ads.common.lib.auth.OfflineCredentials.Api;
import com.google.api.client.auth.oauth2.Credential;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * This example migrates legacy sitelinks to upgraded sitelinks for a given list of campaigns. The
 * campaigns must be upgraded to enhanced campaigns before you can run this example. To upgrade a
 * campaign to enhanced, run campaignmanagement/SetCampaignEnhanced.java. To get all campaigns, run
 * basicoperations/GetCampaigns.java.
 *
 * <p>Credentials and properties in {@code fromFile()} are pulled from the "ads.properties" file.
 * See README for more info.
 *
 * Tags: CampaignAdExtensionService.get, CampaignAdExtensionService.mutate
 * Tags: FeedService.mutate, FeedItemService.mutate, FeedMappingService.mutate
 * Tags: CampaignFeedService.mutate
 *
 * Category: adx-exclude
 *
 * @author Anash P. Oommen
 */
public class UpgradeLegacySitelinks {

  /**
   * Exception thrown when an API operation associated with sitelinks occurs.
   */
  private static class SitelinksException extends Exception {
    /**
     * Constructs a new {@link SitelinksException} with the specified detail message and cause.
     *
     * @param message the detail message
     * @param cause the cause of this exception
     */
    public SitelinksException(String message, Throwable cause) {
      super(message, cause);
    }
  }

  /**
   * Factory for creating an upgraded sitelinks feed.
   */
  private static class SitelinksFeedFactory {

    /**
     * The title for the default feed.
     */
    private static final String SITELINKS_TITLE = "Sitelinks Feed";

    /**
     * The name for the text column of the default feed.
     */
    private static final String SITELINKS_TEXT_COLUMNNAME = "Link Text";

    /**
     * The name for the url column of the default feed.
     */
    private static final String SITELINKS_URL_COLUMNNAME = "Url Text";
    
    /**
     * {@link FeedServiceInterface} instance to work with this {@link Feed}.
     */
    private final FeedServiceInterface feedService;

    /**
     * {@link FeedItemServiceInterface} instance to work with the {@link FeedItem} associated with
     * this {@link Feed}.
     */
    private final FeedItemServiceInterface feedItemService;

    /**
     * {@link FeedMappingServiceInterface} instance to map this {@link Feed} to hold upgraded
     * sitelinks.
     */
    private final FeedMappingServiceInterface feedMappingService;

    /**
     * {@link CampaignFeedServiceInterface} instance to associate {@link FeedItem} in this
     * {@link Feed} with a Campaign.
     */
    private final CampaignFeedServiceInterface campaignFeedService;

    /**
     * The feed id.
     */
    private long id;

    /**
     * Attributes for the text column.
     */
    private FeedAttribute textColumn;

    /**
     * Attributes for the url column.
     */
    private FeedAttribute urlColumn;

    /**
     * Creates an instance of {@link SitelinksFeed}.
     *
     * @param feedService the {@link FeedServiceInterface} instance to work with this {@link Feed}.
     * @param feedMappingService the {@link FeedMappingServiceInterface} instance to map this
     *        {@link Feed} to hold upgraded sitelinks.
     * @param feedItemService the {@link FeedItemServiceInterface} instance to work with the
     *        {@link FeedItem} associated with this {@link Feed}.
     * @param campaignFeedService the {@link CampaignFeedServiceInterface} instance to associate
     *        {@link FeedItem} in this {@link Feed} with a Campaign.
     */
    private SitelinksFeedFactory(FeedServiceInterface feedService,
        FeedMappingServiceInterface feedMappingService, FeedItemServiceInterface feedItemService,
        CampaignFeedServiceInterface campaignFeedService) {
      this.feedService = feedService;
      this.feedMappingService = feedMappingService;
      this.feedItemService = feedItemService;
      this.campaignFeedService = campaignFeedService;
    }

    /**
     * Gets the feed id.
     *
     * @return the feed id.
     */
    private long getId() {
      return id;
    }

    /**
     * Sets the feed id.
     *
     * @param id the feed id to set.
     */
    private void setId(long id) {
      this.id = id;
    }

    /**
     * Gets the attributes for the text column.
     *
     * @return the text column attributes.
     */
    private FeedAttribute getTextColumn() {
      return textColumn;
    }

    /**
     * Sets the attributes for the text column.
     *
     * @param textColumn the text column attributes to set.
     */
    private void setTextColumn(FeedAttribute textColumn) {
      this.textColumn = textColumn;
    }

    /**
     * Gets the attributes for the url column.
     *
     * @return the url column attributes to set.
     */
    private FeedAttribute getUrlColumn() {
      return urlColumn;
    }

    /**
     * Sets the attributes for the url column.
     *
     * @param urlColumn the url column attributes to set.
     */
    private void setUrlColumn(FeedAttribute urlColumn) {
      this.urlColumn = urlColumn;
    }

    /**
     * Gets the default feed for holding upgraded sitelinks.
     *
     * @param createNewFeedIfRequired If true, a new feed will be created if a feed for holding
     *        upgraded sitelinks is missing.
     * @return The initialized {@link SitelinksFeed}.
     * @throws SitelinksException if an exception occurs while retrieving or creating the default
     *         feed for upgraded sitelinks.
     */
    private SitelinksFeed getDefaultFeed(boolean createNewFeedIfRequired)
        throws SitelinksException {
      // Try to retrieve an existing feed that has been mapped for use with
      // sitelinks. if multiple such feeds exist, the first matching feed is
      // retrieved. You could modify this code example to retrieve all the feeds
      // and pick the appropriate feed based on user input.

      if (getDefaultFeed()) {
        return new SitelinksFeed(this.getId(), this.getTextColumn(), this.getUrlColumn(),
            feedItemService, campaignFeedService);
      } else if (createNewFeedIfRequired) {
        // Create a feed for storing sitelinks.
        createFeed(SITELINKS_TITLE, newTextColumn(SITELINKS_TEXT_COLUMNNAME),
            newUrlColumn(SITELINKS_URL_COLUMNNAME));
        mapFeedColumns();
        return new SitelinksFeed(this.getId(), this.getTextColumn(), this.getUrlColumn(),
            feedItemService, campaignFeedService);
      }
      return null;
    }

    /**
     * Retrieve an existing feed that is mapped to hold sitelinks. The first active sitelinks feed
     * is retrieved by this method.
     *
     * @return true if there is a default feed that has been mapped to sitelinks, false otherwise.
     * @throws SitelinksException if an error occurs when retrieving an existing feed.
     */
    private boolean getDefaultFeed() throws SitelinksException {
      Selector selector = new Selector();
      selector.setFields(SitelinksFeed.FEED_SELECTORS);

      Predicate placeHolderPredicate = new Predicate();
      placeHolderPredicate.setField("PlaceholderType");
      placeHolderPredicate.setOperator(PredicateOperator.EQUALS);
      placeHolderPredicate.setValues(new String[] {
          Integer.toString(SitelinksFeed.FeedPlaceholderType.SITELINKS.getNumericValue())});

      Predicate statusPredicate = new Predicate();
      statusPredicate.setField("Status");
      statusPredicate.setOperator(PredicateOperator.EQUALS);
      statusPredicate.setValues(new String[] {"ACTIVE"});

      selector.setPredicates(new Predicate[] {placeHolderPredicate, statusPredicate});

      try {
        FeedMappingPage page = feedMappingService.get(selector);

        if (page != null && page.getEntries() != null && page.getEntries().length > 0) {
          for (FeedMapping feedMapping : page.getEntries()) {
            Long feedId = feedMapping.getFeedId();
            FeedAttribute textColumn = null;
            FeedAttribute urlColumn = null;
            for (AttributeFieldMapping attributeMapping : feedMapping.getAttributeFieldMappings()) {
              if (attributeMapping.getFieldId().intValue()
                  == SitelinksFeed.SitelinksPlaceholders.TEXT.getNumericValue()) {
                textColumn = new FeedAttribute();
                textColumn.setId(attributeMapping.getFeedAttributeId());
              } else if (attributeMapping.getFieldId().intValue()
                  == SitelinksFeed.SitelinksPlaceholders.URL.getNumericValue()) {
                urlColumn = new FeedAttribute();
                textColumn.setId(attributeMapping.getFeedAttributeId());
              }
            }

            this.setId(feedId);
            this.setTextColumn(textColumn);
            this.setUrlColumn(urlColumn);
            return feedId != null && urlColumn != null && textColumn != null;
          }
        }
        return false;
      } catch (RemoteException e) {
        throw new SitelinksException("Failed to retrieve default feed for sitelinks.", e);
      }
    }

    /**
     * Creates a feed for holding upgraded sitelinks.
     *
     * @param name The name of the feed.
     * @param textColumn The feed attributes for the text column.
     * @param urlColumn The feed attributes for the url column.
     * @throws SitelinksException if an error occurs when creating the feed for upgraded sitelinks.
     */
    private void createFeed(String name, FeedAttribute textColumn, FeedAttribute urlColumn)
        throws SitelinksException {
      // Create the feed.
      Feed siteLinksFeed = new Feed();
      siteLinksFeed.setName(name);
      siteLinksFeed.setAttributes(new FeedAttribute[] {textColumn, urlColumn});
      siteLinksFeed.setOrigin(FeedOrigin.USER);

      // Create operation.
      FeedOperation operation = new FeedOperation();
      operation.setOperand(siteLinksFeed);
      operation.setOperator(Operator.ADD);

      // Add the feed.
      FeedReturnValue result;
      try {
        result = feedService.mutate(new FeedOperation[] {operation});

        Feed savedFeed = result.getValue()[0];
        setId(savedFeed.getId());

        // The following index checks are safe, since AdWords API will return exactly the same
        // number of attributes you provided, in the exact same order.
        setTextColumn(savedFeed.getAttributes()[0]);
        setUrlColumn(savedFeed.getAttributes()[1]);
      } catch (RemoteException e) {
        throw new SitelinksException("Failed to create default feed.", e);
      }
    }

    /**
     * Map the feed's columns for use with Sitelinks.
     *
     * @throws SitelinksException if an error occurs when mapping the feed for sitelinks.
     */
    private void mapFeedColumns() throws SitelinksException {
      // Map the FeedAttributeIds to the fieldId constants.
      AttributeFieldMapping linkTextFieldMapping = new AttributeFieldMapping();
      linkTextFieldMapping.setFeedAttributeId(getTextColumn().getId());
      linkTextFieldMapping.setFieldId(SitelinksFeed.SitelinksPlaceholders.TEXT.getNumericValue());

      AttributeFieldMapping linkUrlFieldMapping = new AttributeFieldMapping();
      linkUrlFieldMapping.setFeedAttributeId(getUrlColumn().getId());
      linkUrlFieldMapping.setFieldId(SitelinksFeed.SitelinksPlaceholders.URL.getNumericValue());

      // Create the FieldMapping and operation.
      FeedMapping feedMapping = new FeedMapping();
      feedMapping.setPlaceholderType(SitelinksFeed.FeedPlaceholderType.SITELINKS.getNumericValue());
      feedMapping.setFeedId(getId());
      feedMapping.setAttributeFieldMappings(
          new AttributeFieldMapping[] {linkTextFieldMapping, linkUrlFieldMapping});
      FeedMappingOperation operation = new FeedMappingOperation();
      operation.setOperand(feedMapping);
      operation.setOperator(Operator.ADD);

      try {
        // Save the field mapping.
        feedMappingService.mutate(new FeedMappingOperation[] {operation});
      } catch (RemoteException e) {
        throw new SitelinksException("Failed to map feed columns for sitelinks.", e);
      }
    }

    /**
     * Creates a new text column attribute with default values.
     *
     * @param title The column title.
     * @return A {@link FeedAttribute} object for the text column.
     */
    private static FeedAttribute newTextColumn(String title) {
      FeedAttribute textAttribute = new FeedAttribute();
      textAttribute.setType(FeedAttributeType.STRING);
      textAttribute.setName(title);
      return textAttribute;
    }

    /**
     * Creates a new url column attribute with default values.
     *
     * @param title The column title.
     * @return A {@link FeedAttribute} object for the url column.
     */
    private static FeedAttribute newUrlColumn(String title) {
      FeedAttribute urlAttribute = new FeedAttribute();
      urlAttribute.setType(FeedAttributeType.URL);
      urlAttribute.setName(title);
      return urlAttribute;
    }
  }

  /**
   * Represents a {@link Feed} mapped to hold upgraded sitelinks.
   */
  private static class SitelinksFeed {

    /**
     * Defines the type of feed placeholders supported by AdWords API. See See
     * https://developers.google.com/adwords/api/docs/appendix/placeholders for details.
     */
    private enum FeedPlaceholderType {
      /**
       * Placeholder type for upgraded sitelinks.
       */
      SITELINKS(1);

      /**
       * The numeric value associated with an enumeration item.
       */
      private final int numericValue;

      /**
       * Gets the numeric value associated with this enumeration item.
       *
       * @return the numeric value.
       */
      public int getNumericValue() {
        return numericValue;
      }

      /**
       * Creates a new instance of {@link FeedPlaceholderType}.
       *
       * @param numericValue The numeric value associated with this enumeration item.
       */
      private FeedPlaceholderType(int numericValue) {
        this.numericValue = numericValue;
      }
    }

    /**
     * Defines the sitelink placeholder values. See
     * https://developers.google.com/adwords/api/docs/appendix/placeholders for details.
     */
    private enum SitelinksPlaceholders {
      /**
       * The link text for your sitelink.
       */
      TEXT(1),

      /**
       * The destination URL for your sitelink.
       */
      URL(2),

      /**
       * First line of sitelink description.
       */
      LINE_2(3),

      /**
       * Second line of sitelink description.
       */
      LINE_3(4);

      /**
       * The numeric value associated with this enumeration item.
       */
      private final int numericValue;

      /**
       * Creates a new instance of {@link SitelinksPlaceholders}.
       *
       * @param numericValue The numeric value associated with this enumeration item.
       */
      private SitelinksPlaceholders(int numericValue) {
        this.numericValue = numericValue;
      }

      /**
       * Gets the numeric value associated with this enumeration item.
       *
       * @return the numeric value.
       */
      private int getNumericValue() {
        return numericValue;
      }
    }

    /**
     * The feed id.
     */
    private final long id;

    /**
     * Attributes for the text column.
     */
    private final FeedAttribute textColumn;

    /**
     * Attributes for the url column.
     */
    private final FeedAttribute urlColumn;

    /**
     * List of {@link FeedItem} associated with this {@link Feed}.
     */
    private final List<FeedItem> feedItems = new ArrayList<FeedItem>();

    /**
     * {@link FeedItemServiceInterface} instance to work with the {@link FeedItem} associated with
     * this {@link Feed}.
     */
    private final FeedItemServiceInterface feedItemService;

    /**
     * {@link CampaignFeedServiceInterface} instance to associate {@link FeedItem} in this
     * {@link Feed} with a Campaign.
     */
    private final CampaignFeedServiceInterface campaignFeedService;

    /**
     * Selector fields for retrieving {@link Feed} fields.
     */
    private static final String[] FEED_SELECTORS = new String[] {
        "FeedId", "FeedMappingId", "PlaceholderType", "Status", "AttributeFieldMappings"};

    /**
     * Creates an instance of {@link SitelinksFeed}.
     *
     * @param id The feed id.
     * @param textColumn The feed attribute for text column.
     * @param urlColumn The feed attribute for url column.
     * @param feedItemService the {@link FeedItemServiceInterface} instance to work with the
     *        {@link FeedItem} associated with this {@link Feed}.
     * @param campaignFeedService the {@link CampaignFeedServiceInterface} instance to associate
     *        {@link FeedItem} in this {@link Feed} with a Campaign.
     */
    private SitelinksFeed(long id, FeedAttribute textColumn, FeedAttribute urlColumn,
        FeedItemServiceInterface feedItemService,
        CampaignFeedServiceInterface campaignFeedService) {
      this.id = id;
      this.textColumn = textColumn;
      this.urlColumn = urlColumn;
      this.feedItemService = feedItemService;
      this.campaignFeedService = campaignFeedService;
    }

    /**
     * Gets the list of {@link FeedItem} associated with this {@link Feed}.
     *
     * @return the list of {@link FeedItem}.
     */
    private List<FeedItem> getFeedItems() {
      return feedItems;
    }

    /**
     * Add legacy sitelinks to the upgraded sitelinks feed.
     *
     * @param sitelinks The list of legacy sitelinks to be added to the feed.
     * @return The list of feeditems that were added to the feed.
     * @throws SitelinksException if an error occurred while adding feeditems to the sitelinks feed.
     */
    private FeedItem[] addItems(Sitelink[] sitelinks) throws SitelinksException {
      // Return fast if no action is needed. Round trip to server is costlier.
      if (sitelinks == null || sitelinks.length == 0) {
        return new FeedItem[] {};
      }

      // Create operation for adding each legacy sitelink to the sitelinks feed.
      List<FeedItemOperation> feedItemOperations = new ArrayList<FeedItemOperation>();

      for (Sitelink sitelink : sitelinks) {
        FeedItemOperation operation = newAddItemOperation(sitelink);
        feedItemOperations.add(operation);
      }

      try {
        FeedItemReturnValue result =
            feedItemService.mutate(feedItemOperations.toArray(new FeedItemOperation[] {}));

        this.getFeedItems().addAll(Arrays.asList(result.getValue()));
        return result.getValue();
      } catch (RemoteException e) {
        throw new SitelinksException("Failed to add sitelinks to feed..", e);
      }
    }

    /**
     * Creates a new operation for adding a legacy sitelink into a feed.
     *
     * @param sitelink The legacy sitelink to be added to the feed.
     * @return A {@link FeedItemOperation} for adding the feed item.
     */
    private FeedItemOperation newAddItemOperation(Sitelink sitelink) {
      // Create the FeedItemAttributeValues for our text values.
      FeedItemAttributeValue linkTextAttributeValue = new FeedItemAttributeValue();
      linkTextAttributeValue.setFeedAttributeId(textColumn.getId());
      linkTextAttributeValue.setStringValue(sitelink.getDisplayText());

      FeedItemAttributeValue linkUrlAttributeValue = new FeedItemAttributeValue();
      linkUrlAttributeValue.setFeedAttributeId(urlColumn.getId());
      linkUrlAttributeValue.setStringValue(sitelink.getDestinationUrl());

      // Create the feed item and operation.
      FeedItem item = new FeedItem();
      item.setFeedId(id);
      item.setAttributeValues(
          new FeedItemAttributeValue[] {linkTextAttributeValue, linkUrlAttributeValue});
      FeedItemOperation operation = new FeedItemOperation();
      operation.setOperand(item);
      operation.setOperator(Operator.ADD);
      return operation;
    }

    /**
     * Associates upgraded sitelink feed items with a campaign.
     *
     * @param newItems The list of {@link FeedItem} to be associated with the campaign.
     * @param campaignId The campaign id.
     * @throws SitelinksException if an error occurs when adding upgraded sitelink feed items to the
     *         campaign.
     * @throws IllegalArgumentException if the feed items do not belong to the current feed.
     */
    private void associateItemsWithCampaign(FeedItem[] newItems, long campaignId)
        throws SitelinksException {
      // Verify that all the items belong to the current feed.
      List<FeedItem> feedItemList = this.getFeedItems();
      for (FeedItem item : newItems) {
        if (!feedItemList.contains(item)) {
          throw new IllegalArgumentException(String.format(
              "Feed item id %s doesn't belong to feed Id %s.", item.getFeedItemId(), this.id));
        }
      }

      // Create a custom matching function that matches the given feed items to
      // the campaign.
      RequestContextOperand requestContextOperand = new RequestContextOperand();
      requestContextOperand.setContextType(RequestContextOperandContextType.FEED_ITEM_ID);

      Function function = new Function();
      function.setLhsOperand(new FunctionArgumentOperand[] {requestContextOperand});
      function.setOperator(FunctionOperator.IN);

      List<FunctionArgumentOperand> operands = new ArrayList<FunctionArgumentOperand>();
      for (FeedItem item : newItems) {
        ConstantOperand constantOperand = new ConstantOperand();
        constantOperand.setLongValue(item.getFeedItemId());
        constantOperand.setType(ConstantOperandConstantType.LONG);
        operands.add(constantOperand);
      }
      function.setRhsOperand(operands.toArray(new FunctionArgumentOperand[operands.size()]));

      // Create upgraded sitelinks for the campaign. Use the sitelinks feed we
      // created, and restrict feed items by matching function.
      CampaignFeed campaignFeed = new CampaignFeed();
      campaignFeed.setFeedId(this.id);
      campaignFeed.setCampaignId(campaignId);
      campaignFeed.setMatchingFunction(function);
      campaignFeed.setPlaceholderTypes(new int[] {FeedPlaceholderType.SITELINKS.getNumericValue()});

      CampaignFeedOperation operation = new CampaignFeedOperation();
      operation.setOperand(campaignFeed);
      operation.setOperator(Operator.ADD);
      try {
        campaignFeedService.mutate(new CampaignFeedOperation[] {operation});
      } catch (RemoteException e) {
        throw new SitelinksException("Failed to associate sitelinks with campaign.", e);
      }
    }
  }

  /**
   * Factory for creating an legacy sitelink instance.
   */
  private static class LegacySitelinksFactory {

    /**
     * The {@link CampaignAdExtensionServiceInterface} instance to work with the
     * {@link CampaignAdExtension} object.
     */
    private final CampaignAdExtensionServiceInterface campaignExtensionService;

    /**
     * Creates an instance of {@link LegacySitelink}.
     *
     * @param campaignExtensionService The {@link CampaignAdExtensionServiceInterface} instance to
     *        work with the {@link CampaignAdExtension} object.
     */
    private LegacySitelinksFactory(CampaignAdExtensionServiceInterface campaignExtensionService) {
      this.campaignExtensionService = campaignExtensionService;
    }

    /**
     * Gets the legacy sitelink associated with a campaign.
     *
     * @param campaignId the campaign ID.
     * @return the legacy sitelink.
     * @throws SitelinksException if there was an exception while retrieving the legacy sitelinks
     *         for the campaign.
     */
    private LegacySitelink get(long campaignId) throws SitelinksException {
      return new LegacySitelink(getCampaignAdExtension(campaignId), campaignExtensionService);
    }

    /**
     * Gets legacy sitelinks for a campaign. Retrieved sitelinks are stored in the
     * campaignAdExtension field.
     *
     * @return true if the campaign has legacy sitelinks, false otherwise.
     * @throws SitelinksException if an error occurs when retrieving the legacy sitelinks.
     */
    private CampaignAdExtension getCampaignAdExtension(long campaignId) throws SitelinksException {
      // Create the selector.
      Selector selector = new Selector();
      selector.setFields(LegacySitelink.SELECTOR_FIELDS);

      // Filter the results for specified campaign id.
      Predicate campaignPredicate = new Predicate();
      campaignPredicate.setOperator(PredicateOperator.EQUALS);
      campaignPredicate.setField("CampaignId");
      campaignPredicate.setValues(new String[] {Long.toString(campaignId)});

      // Filter the results for active campaign ad extensions. You may add
      // additional filtering conditions here as required.
      Predicate statusPredicate = new Predicate();
      statusPredicate.setOperator(PredicateOperator.EQUALS);
      statusPredicate.setField("Status");
      statusPredicate.setValues(new String[] {CampaignAdExtensionStatus.ACTIVE.getValue()});

      // Filter for sitelinks ad extension type.
      Predicate typePredicate = new Predicate();
      typePredicate.setOperator(PredicateOperator.EQUALS);
      typePredicate.setField("AdExtensionType");
      typePredicate.setValues(new String[] {"SITELINKS_EXTENSION"});

      selector.setPredicates(new Predicate[] {campaignPredicate, statusPredicate, typePredicate});

      try {
        CampaignAdExtensionPage page = campaignExtensionService.get(selector);
        if (page.getEntries() != null && page.getEntries().length > 0) {
          return page.getEntries()[0];
        } else {
          return null;
        }
      } catch (RemoteException e) {
        throw new SitelinksException("Failed to retrieve legacy sitelinks from campaign.", e);
      }
    }
  }

  /**
   * Represents a {@link CampaignAdExtension} object that holds legacy {@link Sitelink} objects.
   */
  private static class LegacySitelink {

    /**
     * The {@link CampaignAdExtensionServiceInterface} instance to work with the
     * {@link CampaignAdExtension} object.
     */
    private final CampaignAdExtensionServiceInterface campaignExtensionService;

    /**
     * The selector fields for retrieving {@link Sitelink}.
     */
    private static final String[] SELECTOR_FIELDS =
        new String[] {"CampaignId", "AdExtensionId", "Status", "DisplayText", "DestinationUrl"};

    /**
     * The campaign ad extension that holds the sitelinks.
     */
    private CampaignAdExtension campaignAdExtension;

    /**
     * Creates an instance of {@link LegacySitelink}.
     *
     * @param campaignAdExtension The {@link CampaignAdExtension} associated with this object.
     * @param campaignExtensionService The {@link CampaignAdExtensionServiceInterface} instance to
     *        work with the {@link CampaignAdExtension} object.
     */
    private LegacySitelink(CampaignAdExtension campaignAdExtension,
        CampaignAdExtensionServiceInterface campaignExtensionService) {
      this.campaignAdExtension = campaignAdExtension;
      this.campaignExtensionService = campaignExtensionService;
    }

    /**
     * Gets the legacy sitelinks associated with this instance.
     *
     * @return An array of {@link Sitelink} objects, or an empty array if there are no sitelinks
     *         associated with this campaign.
     */
    private Sitelink[] items() {
      if (campaignAdExtension != null && campaignAdExtension.getAdExtension() != null
          && campaignAdExtension.getAdExtension() instanceof SitelinksExtension) {
        return ((SitelinksExtension) campaignAdExtension.getAdExtension()).getSitelinks();
      } else {
        return new Sitelink[] {};
      }
    }

    /**
     * Deletes legacy sitelinks associated with this object.
     *
     * @throws SitelinksException if an error occurs when deleting the legacy sitelinks.
     */
    private void delete() throws Exception {
      CampaignAdExtensionOperation operation = new CampaignAdExtensionOperation();
      operation.setOperator(Operator.REMOVE);
      operation.setOperand(campaignAdExtension);

      try {
        CampaignAdExtensionReturnValue retval =
            campaignExtensionService.mutate(new CampaignAdExtensionOperation[] {operation});
        // This assignment is safe, the AdWords server will return the same number of items passed
        // to it, or will raise an exception if there is an issue with the request.
        campaignAdExtension = retval.getValue(0);
      } catch (RemoteException e) {
        throw new SitelinksException("Failed to delete legacy sitelinks from campaign.", e);
      }
    }
  }

  /**
   * The entry point for this code example.
   *
   * @param args The command line arguments
   * @throws Exception if an exception occurs when running this code example.
   */
  public static void main(String[] args) throws Exception {
    // Generate a refreshable OAuth2 credential similar to a ClientLogin token
    // and can be used in place of a service account.
    Credential oAuth2Credential = new OfflineCredentials.Builder().forApi(Api.ADWORDS)
        .fromFile().build().generateCredential();

    // Construct an AdWordsSession.
    AdWordsSession session =
        new AdWordsSession.Builder().fromFile().withOAuth2Credential(oAuth2Credential).build();

    long[] campaignIds =
        {Long.parseLong("INSERT_CAMPAIGN_ID_HERE"), Long.parseLong("INSERT_CAMPAIGN_ID_HERE")};
    AdWordsServices adWordsServices = new AdWordsServices();

    runExample(adWordsServices, session, campaignIds);
  }

  /**
   * Runs the code example.
   *
   * @param adWordsServices The {@link AdWordsServices} for creating service instances.
   * @param session The session for running the code example.
   * @param campaignIds The campaign ids for which legacy sitelinks are to be migrated.
   * @throws Exception if an exception occurs when running this code example.
   */
  public static void runExample(
      AdWordsServices adWordsServices, AdWordsSession session, long[] campaignIds)
      throws Exception {
    // Get the CampaignAdExtensionService.
    CampaignAdExtensionServiceInterface campaignExtensionService =
        adWordsServices.get(session, CampaignAdExtensionServiceInterface.class);
    // Get the FeedMappingService.
    FeedMappingServiceInterface feedMappingService =
        adWordsServices.get(session, FeedMappingServiceInterface.class);
    // Get the FeedService.
    FeedServiceInterface feedService = adWordsServices.get(session, FeedServiceInterface.class);
    // Get the FeedItemService.
    FeedItemServiceInterface feedItemService =
        adWordsServices.get(session, FeedItemServiceInterface.class);
    // Get the CampaignFeedService.
    CampaignFeedServiceInterface campaignFeedService =
        adWordsServices.get(session, CampaignFeedServiceInterface.class);

    SitelinksFeedFactory feedFactory = new SitelinksFeedFactory(
        feedService, feedMappingService, feedItemService, campaignFeedService);

    SitelinksFeed feed = feedFactory.getDefaultFeed(true);

    LegacySitelinksFactory legacySitelinksFactory =
        new LegacySitelinksFactory(campaignExtensionService);

    for (long campaignId : campaignIds) {
      LegacySitelink legacySitelink = legacySitelinksFactory.get(campaignId);
      // Add the sitelink items to the new feed.
      FeedItem[] newItems = feed.addItems(legacySitelink.items());

      // Associate feeditems to the campaign.
      feed.associateItemsWithCampaign(newItems, campaignId);

      // Once the upgraded sitelinks are added to a campaign, the legacy
      // sitelinks will stop serving. You can delete the legacy sitelinks
      // once you have verified that the migration went fine. In case the
      // migration didn't succeed, you can roll back the migration by deleting
      // the CampaignFeed you created in the previous step.
      legacySitelink.delete();
    }
  }
}
